using System;
using System.Linq;
using System.Web;
using Altairis.Web.Management.Configuration;

namespace Altairis.Web.Management {
    class RedirectorModule : IHttpModule {
        private ManagementSection config;
        private string[] hostVerbs;
        private string[] sslVerbs;

        /// <summary>
        /// Disposes of the resources (other than memory) used by the module that implements <see cref="T:System.Web.IHttpModule"/>.
        /// </summary>
        public void Dispose() {
            // NOOP
        }

        /// <summary>
        /// Initializes a module and prepares it to handle requests.
        /// </summary>
        /// <param name="context">An <see cref="T:System.Web.HttpApplication"/> that provides access to the methods, properties, and events common to all application objects within an ASP.NET application</param>
        public void Init(HttpApplication context) {
            // Load configuration
            this.config = ManagementSection.GetCurrentOrDefault();

            if (this.config.SslRedirector.Enabled && (this.config.SslRedirector.Rules.Count != 0 || this.config.SslRedirector.DefaultAction != SslRedirectorAction.None)) {
                // Enable SSL redirector
                context.BeginRequest += new EventHandler(SslRedirector_BeginRequest);

                // Load its verbs
                this.sslVerbs = config.SslRedirector.Verbs.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
            }

            if (this.config.HostRedirector.Enabled && !string.IsNullOrEmpty(this.config.HostRedirector.HostName)) {
                // Enable host redirector
                context.BeginRequest += new EventHandler(HostRedirector_BeginRequest);

                // Load its verbs
                this.hostVerbs = config.HostRedirector.Verbs.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
            }

        }

        #region Host redirector

        /// <summary>
        /// Handles the BeginRequest event of the HostRedirector control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        public void HostRedirector_BeginRequest(object sender, EventArgs e) {
            var app = sender as HttpApplication;

            // Check if verb is enabled
            if (!this.hostVerbs.Any(x => x.Equals(app.Request.HttpMethod, StringComparison.OrdinalIgnoreCase))) return;

            // Check if host name is local or correct
            if (this.config.HostRedirector.ExcludeLocalRequests && app.Request.IsLocal) return;
            if (app.Request.Url.Host.Equals(this.config.HostRedirector.HostName, StringComparison.OrdinalIgnoreCase)) return;

            // Build URL for redirection
            UriBuilder newUrl = new UriBuilder(app.Request.Url);
            newUrl.Host = this.config.HostRedirector.HostName;

            // Redirect
            if (this.config.HostRedirector.Permanent) {
                app.Response.RedirectPermanent(newUrl.Uri.ToString());
            }
            else {
                app.Response.Redirect(newUrl.Uri.ToString());
            }
        }

        #endregion

        #region SSL redirector

        /// <summary>
        /// Handles the BeginRequest event of the SslRedirector control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        public void SslRedirector_BeginRequest(object sender, EventArgs e) {
            var app = sender as HttpApplication;
            var virtualPath = app.Request.AppRelativeCurrentExecutionFilePath;

            // Check if verb is enabled
            if (!this.sslVerbs.Any(x => x.Equals(app.Request.HttpMethod, StringComparison.OrdinalIgnoreCase))) return;

            // Skip local requests if required
            if (this.config.HostRedirector.ExcludeLocalRequests && app.Request.IsLocal) return;

            // Skip system URLs if required
            if (this.config.SslRedirector.ExcludeSystemUrls && app.Request.IsInfrastructureRequest()) return;

            // Perform required action
            var isSecure = app.Request.IsSecureConnection;
            var action = EvaluateSslRules(virtualPath);
            if (action == SslRedirectorAction.None) {
                // no action necessary
                return;
            }
            else if (action == SslRedirectorAction.ForceSecure && !isSecure) {
                // redirect to secure version
                PerformSslRedirect(true);
            }
            else if (action == SslRedirectorAction.ForceUnsecure && isSecure) {
                // redirect to unsecure version
                PerformSslRedirect(false);
            }
        }

        /// <summary>
        /// Performs the SSL redirect.
        /// </summary>
        /// <param name="secure">If set to <c>true</c>, redirects to HTTPS version, to HTTP otherwise.</param>
        private void PerformSslRedirect(bool secure) {
            UriBuilder newUrl = new UriBuilder(HttpContext.Current.Request.Url);
            if (secure) {
                // Redirect to HTTPS
                newUrl.Scheme = "https://";
                newUrl.Port = this.config.SslRedirector.SecurePort;

                // Check if browser is SNI capable
                var supportsSni = IsSniCapable(HttpContext.Current.Request.UserAgent);
                if (!supportsSni) {
                    // Browser is not SNI capable
                    switch (this.config.SslRedirector.SniFallbackAction) {
                        case SniFallbackAction.UseUnsecure:
                            // Allow unsecure requests for non-SNI browsers - do nothing
                            return;
                        case SniFallbackAction.UseAlternatePort:
                            // Use alternate port
                            newUrl.Port = this.config.SslRedirector.SniAlternatePort; break;
                        case SniFallbackAction.Off:
                        default:
                            // No SNI fallback at all
                            break;
                    }
                }
            }
            else {
                // Redirect to HTTP
                newUrl.Scheme = "http://";
                newUrl.Port = this.config.SslRedirector.UnsecrePort;
            }
            HttpContext.Current.Response.Redirect(newUrl.Uri.ToString());
        }

        /// <summary>
        /// Evaluates the SSL rules.
        /// </summary>
        /// <param name="virtualPath">The virtual path.</param>
        /// <returns>Redirector action resulting from evaluation of rules.</returns>
        private SslRedirectorAction EvaluateSslRules(string virtualPath) {
            var matchingRule = this.config.SslRedirector.Rules.Cast<SslRedirectorRuleElement>().FirstOrDefault(x => virtualPath.StartsWith(x.VirtualPath, StringComparison.OrdinalIgnoreCase));
            if (matchingRule == null) return this.config.SslRedirector.DefaultAction;
            return matchingRule.Action;
        }

        /// <summary>
        /// Check if given user agent supports Server Name Indication
        /// </summary>
        /// <param name="userAgent">The User Agent string</param>
        /// <returns><c>true</c> if user agent supports SNI, <c>false</c> otherwise.</returns>
        private static bool IsSniCapable(string userAgent) {
            if (string.IsNullOrWhiteSpace(userAgent)) return false;                                 // Unknown agent
            if (userAgent.Contains("MSIE") && userAgent.Contains("Windows NT 5.1")) return false;   // IE on Windows XP
            return true;
        }

        #endregion

    }
}
